/**
* Copyright (c) By zengqh.
*
* This program is just for fun or demo, in the hope that it  
* will be useful, you can redistribute it and/or modify freely.
*
* Time: 2012/07/11
* File: OGLRenderSystem.cpp
* Blog: http://www.cnblogs.com/zengqh/
**/

#include "GLSLEffect.h"
#include "../MaterialData.h"
#include "../LightData.h"
#include "OGLTexture2D.h"
#include "OGLRenderSystem.h"
#include "../EffectService.h"
#include "OGLGraphicsBuffer.h"
#include "OGLRenderTT.h"

#define		BUFFER_OFFSET(bytes)		((GLubyte*)NULL + (bytes))

OGLRenderSystem::OGLRenderSystem()
: _rs_state(RS_OGL)
, _is_reset_material_data(true)
, _max_light_num(8)					/* 8 light max */
, _glsl_effect(NULL)
, _effect_service(NULL)
, _render_target(NULL)
{
	_light_data_list.resize(MaxLightSupport);
}

OGLRenderSystem::~OGLRenderSystem()
{

}

bool OGLRenderSystem::beginScene()
{
	clearColor(glm::vec4(0.0f, 0.0f, 0.0f, 1.0f));
	clearZBuffer();

	applyFrameStarted();

	return true;
}

bool OGLRenderSystem::endScene()
{
	applyFrameEnded();

	glFlush();

	return true;
}

void OGLRenderSystem::setTransform(TransformState state, const glm::mat4& transform)
{
	_matrix_list[state] = transform;
}

const glm::mat4& OGLRenderSystem::getTransform(TransformState state) const
{
	return _matrix_list[state];
}

void OGLRenderSystem::setViewport(const Recti& view_port)
{
	_view_port = view_port;
	_screen_view_port = _view_port;
}

const Recti& OGLRenderSystem::getViewport() const
{
	return _view_port;
}

s32 OGLRenderSystem::getType() const
{
	return _rs_state;
}

void OGLRenderSystem::bindBuffer(GLenum target, GLuint buffer)
{
	glBindBuffer(target, buffer);
}

void OGLRenderSystem::deleteBuffers(GLsizei n, GLuint* buffers)
{
	glDeleteBuffers(n, buffers);
}


void OGLRenderSystem::setFVF(FVFState fvf)
{
	_fvf_state = fvf;
}

void OGLRenderSystem::setGraphicsBuffer(BufferState state, 
										GraphicsBuffer* graphics_buffer)
{
	_gb_list[state] = graphics_buffer;
}

void OGLRenderSystem::setEffect(Effect* effect)
{
	_glsl_effect = static_cast<GLSLEffect*>(effect);
}

void OGLRenderSystem::setMaterial(MaterialData* material_data)
{
	_material_data = material_data;
}

const MaterialData& OGLRenderSystem::getMaterial() const
{
	return *_material_data;
}

void OGLRenderSystem::addLight(LightData* light_data)
{
	for (s32 i = 0; i < _max_light_num; ++i)
	{
		LightData* data = _light_data_list[i];
		if (!data || !data->_enable)
		{
			light_data->_enable = true;
			light_data->_index = i;
			_light_data_list[i] = light_data;

			break;
		}
	}
}

const LightData& OGLRenderSystem::getLight(int index) const
{
	for (s32 i = 0; i < _max_light_num; ++i)
	{
		LightData* data = _light_data_list[i];
		if (data->_index == index)
		{
			return *data;
		}
	}

	return _dummy_light;
}

void OGLRenderSystem::deleteAllLights()
{
	for (s32 i = 0; i < _max_light_num; ++i)
	{
		LightData* data = _light_data_list[i];

		if (data)
		{
			data->_enable = false;
		}
	}
}


void OGLRenderSystem::drawPrimitiveIndex(u32 primitive_count, 
										 PrimitiveState mode, u32 offset, bool is_index_16)
{
	GLenum gl_mode ;
	GLenum type;
	size_t bytes;

	switch(mode)
	{
	case PT_POINTS:
		gl_mode = GL_POINTS;
		break;
	case PT_LINE_STRIP:
		gl_mode = GL_LINE_STRIP;
		break;
	case PT_LINE_LOOP:
		gl_mode = GL_LINE_LOOP;
		break;
	case PT_LINES:
		gl_mode = GL_LINES;
		break;
	case PT_TRIANGLE_STRIP:
		gl_mode = GL_TRIANGLE_STRIP;
		break;
	case PT_TRIANGLE_FAN:
		gl_mode = GL_TRIANGLE_FAN;
		break;
	case PT_TRIANGLES:
		gl_mode = GL_TRIANGLES;
		break;
	case PT_QUAD_STRIP:
		gl_mode = GL_QUAD_STRIP;
		break;
	case PT_QUADS:
		gl_mode = GL_QUADS;
		break;
	case PT_POLYGON:
		gl_mode = GL_POLYGON;
		break;
	case PT_POINT_SPRITES:
		gl_mode = GL_POINT_SPRITE;
		break;
	default:
		rf_assert(0);
		break;
	}

	if (is_index_16)
	{
		type = GL_UNSIGNED_SHORT;
		bytes = sizeof(GLushort);
	}
	else
	{
		type = GL_UNSIGNED_INT;
		bytes = sizeof(GLint);
	}


	applyEffect();

	applyLight();

	applyMaterial();

	applyMatrix();

	applyGraphicsBuffer();

	applyFVF(true);

	glDrawElements(gl_mode, primitive_count, type, (GLvoid*)(bytes * offset));

	applyFVF(false);

#ifdef GLSL_SUPPORT_ENABLE
	setEffectService(NULL);
#endif
}

void OGLRenderSystem::applyFVF(bool en)
{
	/* this for glsl 1.40 */
#ifdef GLSL_SUPPORT_ENABLE
	s32 stride = 0;
	s32 vertex_offset = 0;
	s32 normal_offset = 0;
	s32 color_offset = 0;
	s32 texture_offset = 0;

	FVFState fvf = _fvf_state;

	void (APIENTRY *fnSet)(GLuint) = en ? glEnableVertexAttribArray : glDisableVertexAttribArray;

	if (fvf & FVF_V3F)
	{
		vertex_offset = stride;
		stride += 3 * sizeof(GLfloat);
	}
	else if (fvf & FVF_V2F)
	{
		vertex_offset = stride;
		stride += 2 * sizeof(GLfloat);
	}

	if (fvf & FVF_N3F)
	{
		normal_offset = stride;
		stride += 3 * sizeof(GLfloat);
	}

	if (fvf & FVF_C4UB)
	{
		color_offset = stride;
		stride += 4 * sizeof(GLfloat);
	}

	if (fvf & FVF_T2F)
	{
		texture_offset = stride;
		stride += 2 * sizeof(GLfloat);
	}

	if (en)
	{
		if (fvf & FVF_V3F)
		{
			s32 handle = _effect_service->getVertexHandle();

			if (handle != -1)
			{
				fnSet(handle);
				glVertexAttribPointer(handle, 3, GL_FLOAT, GL_FALSE, stride, BUFFER_OFFSET(vertex_offset));
			}
		}
		else if (fvf & FVF_V2F)
		{
			s32 handle = _effect_service->getVertexHandle();
			if (handle != -1)
			{
				fnSet(handle);
				glVertexAttribPointer(handle, 2, GL_FLOAT, GL_FALSE, stride, BUFFER_OFFSET(vertex_offset));
			}
		}

		if (fvf & FVF_N3F)
		{
			s32 handle = _effect_service->getNormalHandle();
			if (handle != -1)
			{
				fnSet(handle);
				glVertexAttribPointer(handle, 3, GL_FLOAT, GL_FALSE, stride, BUFFER_OFFSET(normal_offset));
			}
		}

		if (fvf & FVF_C4UB)
		{
			s32 handle = _effect_service->getColorHandle();
			if (handle != -1)
			{
				fnSet(handle);
				glVertexAttribPointer(handle, 4, GL_FLOAT, GL_FALSE, stride, BUFFER_OFFSET(color_offset));
			}
		}

		if (fvf & FVF_T2F)
		{
			s32 handle = _effect_service->getTexcoordHandle();

			if (handle != -1)
			{
				fnSet(handle);
				glVertexAttribPointer(handle, 2, GL_FLOAT, GL_FALSE, stride, BUFFER_OFFSET(texture_offset));
			}
		}
	}
	else
	{
		if (fvf & FVF_V3F)
		{
			s32 handle = _effect_service->getVertexHandle();
			if (handle != -1) fnSet(handle);
		}
		else if (fvf & FVF_V2F)
		{
			s32 handle = _effect_service->getVertexHandle();
			if (handle != -1) fnSet(handle);
		}

		if (fvf & FVF_N3F)
		{
			s32 handle = _effect_service->getNormalHandle();
			if (handle != -1) fnSet(handle);
		}

		if (fvf & FVF_C4UB)
		{
			s32 handle = _effect_service->getColorHandle();
			if (handle != -1) fnSet(handle);
		}

		if (fvf & FVF_T2F)
		{
			s32 handle = _effect_service->getTexcoordHandle();
			if (handle != -1) fnSet(handle);
		}
	}
#else
	s32 stride = 0;
	s32 vertex_offset = 0;
	s32 normal_offset = 0;
	s32 color_offset = 0;
	s32 texture_offset = 0;

	FVFState fvf = _fvf_state;

	void (APIENTRY *fnSet)(GLenum) = en ? glEnableClientState : glDisableClientState;

	if (fvf & FVF_V3F)
	{
		fnSet(GL_VERTEX_ARRAY);
		vertex_offset = stride;
		stride += 3 * sizeof(f32);
	}
	else if (fvf & FVF_V2F)
	{
		fnSet(GL_VERTEX_ARRAY);
		vertex_offset = stride;
		stride += 2 * sizeof(f32);
	}

	if (fvf & FVF_N3F)
	{
		fnSet(GL_NORMAL_ARRAY);
		normal_offset = stride;
		stride += 3 * sizeof(f32);
	}

	if (fvf & FVF_C4UB)
	{
		fnSet(GL_COLOR_ARRAY);
		color_offset = stride;
		stride += 4 * sizeof(f32);
	}

	if (fvf & FVF_T2F)
	{
		fnSet(GL_TEXTURE_COORD_ARRAY);

		texture_offset = stride;
		stride += 2 * sizeof(f32);
	}

	
	if (en)
	{
		if (fvf & FVF_V3F)
		{
			glVertexPointer(3, GL_FLOAT, stride, BUFFER_OFFSET(vertex_offset));
		}
		else if (fvf & FVF_V2F)
		{
			glVertexPointer(2, GL_FLOAT, stride, BUFFER_OFFSET(vertex_offset));
		}

		if (fvf & FVF_N3F)
		{
			glNormalPointer(GL_FLOAT, stride, BUFFER_OFFSET(normal_offset));
		}

		if (fvf & FVF_C4UB)
		{
			glColorPointer(4, GL_UNSIGNED_BYTE, stride, BUFFER_OFFSET(color_offset));
		}

		if (fvf & FVF_T2F)
		{
			glTexCoordPointer(2, GL_FLOAT, stride, BUFFER_OFFSET(texture_offset));
		}
	}
#endif
}

void OGLRenderSystem::applyMatrix()
{
	glViewport(_view_port._left_top.x, _view_port._left_top.y,
		_view_port._right_bottom.x, _view_port._right_bottom.y);

#ifdef GLSL_SUPPORT_ENABLE
	if (_effect_service)
	{
		_effect_service->applyMatrix();
	}
#else

	/* fixed render pipeline, in opengl 2.0 */
	glMatrixMode(GL_MODELVIEW);

	glm::mat4 mv = _matrix_list[TS_View] * _matrix_list[TS_World];
	glLoadMatrixf(glm::value_ptr(mv));

	glMatrixMode(GL_PROJECTION);
	glLoadMatrixf(glm::value_ptr(_matrix_list[TS_Projection]));

#endif
}

void OGLRenderSystem::applyGraphicsBuffer()
{
	_gb_list[BS_VERTEX_BUFFER]->active();
	_gb_list[BS_INDEX_BUFFER]->active();
}

void OGLRenderSystem::applyEffect()
{
#ifdef GLSL_SUPPORT_ENABLE
	if (_effect_service)
	{
		_effect_service->active();
	}
#endif
}

void OGLRenderSystem::applyMaterial()
{
	if (!_material_data) return;

	/* glMaterialfv will cost some resource, 
	 * call it as less as you can.
	 */
	if (_effect_service)
	{
		_effect_service->applyMaterial();
	}
	
	/* texture */
	for(u32 i = 0; i < MaxTextureSupport; ++i)
	{
		if (!_material_data->getTexture(i)) continue;

		glActiveTexture(GL_TEXTURE0 + i);

		glEnable(GL_TEXTURE_2D);
		glBindTexture(GL_TEXTURE_2D, _material_data->getTexture(i)->getTextureID());

		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER,
			_material_data->_texture_layer_list[i]._is_linear_filter ? GL_LINEAR : GL_NEAREST);

		if (_material_data->getTexture(i)->isMipmap())
		{
			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
				_material_data->_texture_layer_list[i]._is_linear_filter ? GL_LINEAR_MIPMAP_LINEAR : 
				GL_NEAREST_MIPMAP_NEAREST);
		}
		else
		{
			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
				_material_data->_texture_layer_list[i]._is_linear_filter ? GL_LINEAR : GL_NEAREST);
		}

		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, 
			getTextureWrapMode(_material_data->_texture_layer_list[i]._texture_wrap_u));

		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, 
			getTextureWrapMode(_material_data->_texture_layer_list[i]._texture_wrap_v));

	}

	/* is gouraud ? */
	if (_is_reset_material_data || _material_data->_gouraud_shading != _last_material_data._gouraud_shading )
	{
		if (_material_data->_gouraud_shading)
		{
			glShadeModel(GL_SMOOTH);
		}
		else
		{
			glShadeModel(GL_FLAT);
		}

		_last_material_data._gouraud_shading = _material_data->_gouraud_shading;
	}

	/* is wire frame ? */
	if (_is_reset_material_data || _material_data->_wireframe != _last_material_data._wireframe)
	{
		glPolygonMode(GL_FRONT_AND_BACK, _material_data->_wireframe ? GL_LINE : GL_FILL);
		_last_material_data._wireframe = _material_data->_wireframe;
	}

	if (_is_reset_material_data || _material_data->_lighting != _last_material_data._lighting)
	{
		if (_material_data->_lighting)
		{
			glEnable(GL_LIGHTING);
		}
		else
		{
			glDisable(GL_LIGHTING);
		}

		_last_material_data._lighting = _material_data->_lighting;
	}

	if (_is_reset_material_data || _material_data->_zbuffer != _last_material_data._zbuffer)
	{
		switch(_material_data->_zbuffer)
		{
		case ECFN_NEVER:
			glDisable(GL_DEPTH_TEST);
			break;
		case ECFN_LESSEQUAL:
			glEnable(GL_DEPTH_TEST);
			glDepthFunc(GL_LEQUAL);
			break;
		case ECFN_EQUAL:
			glEnable(GL_DEPTH_TEST);
			glDepthFunc(GL_EQUAL);
			break;
		case ECFN_LESS:
			glEnable(GL_DEPTH_TEST);
			glDepthFunc(GL_LESS);
			break;
		case ECFN_NOTEQUAL:
			glEnable(GL_DEPTH_TEST);
			glDepthFunc(GL_NOTEQUAL);
			break;
		case ECFN_GREATEREQUAL:
			glEnable(GL_DEPTH_TEST);
			glDepthFunc(GL_GEQUAL);
			break;
		case ECFN_GREATER:
			glEnable(GL_DEPTH_TEST);
			glDepthFunc(GL_GREATER);
			break;
		case ECFN_ALWAYS:
			glEnable(GL_DEPTH_TEST);
			glDepthFunc(GL_ALWAYS);
			break;
		}
	}

	/* why not _is_reset_material_data and ... */
	if (_material_data->_zbuffer)
	{
		glDepthMask(GL_TRUE);
	}
	else
	{
		glDepthMask(GL_FALSE);
	}

	if (_is_reset_material_data || _material_data->_back_face_culling != _last_material_data._back_face_culling
		|| _material_data->_front_face_culling != _last_material_data._front_face_culling)
	{
		if ((_material_data->_front_face_culling) && (_material_data->_back_face_culling))
		{
			glCullFace(GL_FRONT_AND_BACK);
			glEnable(GL_CULL_FACE);
		}
		else if (_material_data->_back_face_culling)
		{
			glCullFace(GL_BACK);
			glEnable(GL_CULL_FACE);
		}
		else if (_material_data->_front_face_culling)
		{
			glCullFace(GL_FRONT);
			glEnable(GL_CULL_FACE);
		}
		else
		{
			glDisable(GL_CULL_FACE);
		}

		_last_material_data._back_face_culling = _material_data->_back_face_culling;
		_last_material_data._front_face_culling = _material_data->_front_face_culling;
	}

	_is_reset_material_data = false;
}

void OGLRenderSystem::applyLight()
{
#ifdef GLSL_SUPPORT_ENABLE
	if (_effect_service)
	{
		_effect_service->applyLight();
	}
#else

	GLfloat data[4];
	for (int i = 0; i < _light_data_list.size(); ++i)
	{
		int li = GL_LIGHT0 + i;
		LightData* light_data = _light_data_list[i];

		switch (light_data->_type)
		{
		case LT_POINT:
			{
				data[0] = light_data->_position.x;
				data[1] = light_data->_position.y;
				data[2] = light_data->_position.z;
				data[3] = 1.0f; // 1.0f for positional light

				glLightfv(li, GL_POSITION, data);

				glLightf(li, GL_SPOT_EXPONENT, 0.0f);
				glLightf(li, GL_SPOT_CUTOFF, 180.0f);
			}
			break;

		case LT_SPOT:
			{
				data[0] = light_data->_direction.x;
				data[1] = light_data->_direction.y;
				data[2] = light_data->_direction.z;
				data[3] = 0.0f;
				glLightfv(li, GL_SPOT_DIRECTION, data);

				// set position
				data[0] = light_data->_position.x;
				data[1] = light_data->_position.y;
				data[2] = light_data->_position.z;
				data[3] = 1.0f; // 1.0f for positional light

				glLightfv(li, GL_POSITION, data);

				glLightf(li, GL_SPOT_EXPONENT, light_data->_fall_off);
				glLightf(li, GL_SPOT_CUTOFF, light_data->_outer_cone);
			}
			break;
		case LT_DIRECTIONAL:
			{
				data[0] = -light_data->_direction.x;
				data[1] = -light_data->_direction.y;
				data[2] = -light_data->_direction.z;
				data[3] = 0.0f; // 0.0f for directional light

				glLightfv(li, GL_POSITION, data);

				glLightf(li, GL_SPOT_EXPONENT, 0.0f);
				glLightf(li, GL_SPOT_CUTOFF, 180.0f);
			}
			break;
		}

		glLightf(li, GL_CONSTANT_ATTENUATION, light_data->_attenuation.x);
		glLightf(li, GL_LINEAR_ATTENUATION, light_data->_attenuation.y);
		glLightf(li, GL_QUADRATIC_ATTENUATION, light_data->_attenuation.z);

		glEnable(li);
	}
#endif
}

/* make it simple, i will do it later */
GLint OGLRenderSystem::getTextureWrapMode(const u8 clamp)
{
	GLint mode = GL_REPEAT;

	switch(clamp)
	{
	case ETC_REPEAT:
		mode=GL_REPEAT;
		break;
	case ETC_CLAMP:
		mode=GL_CLAMP;
		break;
	case ETC_CLAMP_TO_EDGE:
		mode=GL_CLAMP_TO_EDGE;
		break;
	}

	return mode;
}


Texture* OGLRenderSystem::createTexture(const char* path, bool is_mipmap)
{
	OGLTexture2D* texture = new OGLTexture2D(path, is_mipmap);

	return texture;
}

GraphicsBuffer* OGLRenderSystem::createGraphicsBuffer(
	BufferUsage usage, 
	u32 access_hint, 
	BufferState target, 
	void* init_data, 
	u32 size)
{
	GLenum t;

	switch(target)
	{
	case BS_VERTEX_BUFFER:
		t = GL_ARRAY_BUFFER;
		break;
	case BS_INDEX_BUFFER:
		t = GL_ELEMENT_ARRAY_BUFFER;
		break;
	default:
		rf_assert(0);
	}

	return new OGLGraphicsBuffer(usage, access_hint, t, init_data, 0);
}

void OGLRenderSystem::clearColor(const glm::vec4& color)
{
	GLbitfield mask = 0;
	mask |= GL_COLOR_BUFFER_BIT;
	glClearColor(color.r, color.g, color.b, color.a);
	glClear(mask);
}

void OGLRenderSystem::clearZBuffer()
{
	GLbitfield mask = 0;
	mask |= GL_DEPTH_BUFFER_BIT;
	glDepthMask(GL_TRUE);

	glClear(mask);
}

void OGLRenderSystem::addFrameListener(FrameListener* listener)
{
	_frame_listener_list.push_back(listener);
}

bool OGLRenderSystem::applyFrameStarted()
{
	FrameEvent event;	/* empty now */

	FrameListenerList::iterator iter = _frame_listener_list.begin();
	for(; iter != _frame_listener_list.end(); ++iter)
	{
		if ((*iter)->frameStarted(event))
		{
			return false;
		}
	}

	return true;
}

bool OGLRenderSystem::applyFrameEnded()
{
	FrameEvent event;	/* empty now */

	FrameListenerList::iterator iter = _frame_listener_list.begin();
	for(; iter != _frame_listener_list.end(); ++iter)
	{
		if ((*iter)->frameEnded(event))
		{
			return false;
		}
	}

	return true;
}

void OGLRenderSystem::setEffectService(EffectService* es)
{
	_effect_service = es;
}

Effect* OGLRenderSystem::createEffect()
{
	return new GLSLEffect();
}

void OGLRenderSystem::setRenderTarget(RenderTarget* rt)
{
	if (_render_target)
	{
		_render_target->unbind();
		_view_port = _screen_view_port;
	}

	if (rt)
	{
		rt->bind();

		_view_port._right_bottom.x = rt->getWidth();
		_view_port._right_bottom.y = rt->getHeight();

		_render_target = rt;
	}
	else
	{
		//glDrawBuffer(GL_BACK_LEFT);
	}
}

RenderTarget* OGLRenderSystem::createRTT(const Dimension2Di& size)
{
	return new OGLRenderTT(size);
}